/**************************/
/* Handles IRC Connection */
/**************************/

/*
 * Numeric responses
 * I only grabbed the ones I figured were relevant to a light-weight irc client
 */

var NUMERIC = {
	"RPL_AWAY": "301",
	"RPL_UNAWAY": "305",
	"RPL_NOWAWAY": "306",
	"RPL_WHOISUSER": "311",
	"RPL_WHOISIDLE": "317",
	"RPL_ENDOFWHOIS": "318",
	"RPL_WHOISCHANNELS": "319",
	"RPL_LISTSTART": "321",
	"RPL_LIST": "322",
	"RPL_LISTEND": "323",
	"RPL_CHANNELMODEIS": "324",
	"RPL_NOTOPIC": "331",
	"RPL_TOPIC": "332",
	"RPL_INVITING": "341",
	"RPL_VERSION": "351",
	"RPL_NAMREPLY": "353",
	"RPL_ENDOFNAMES": "366",
	"RPL_MOTDSTART": "375",
	"RPL_MOTD": "372",
	"RPL_ENDOFMOTD": "376",
	"ERR_NOMOTD": "422",
	"RPL_LUSERCLIENT": "251",
	"ERR_ERRONEUSNICKNAME": "432",
	"ERR_NICKNAMEINUSE": "433",
	"ERR_NICKCOLLISION": "436"
};

/* I wouldn't suggest changing this unless you have a really really good reason */
var MAX_BUFFER_SIZE = 512;

/*
 * === IrcConnection Object ===
 * 
 * *** Params: ***
 * host (string)
 * port (integer)
 * Options (js object)
 *  callbacks			object with the following:
 * 		connect			callback for successful connection [IrcConnection]
 * 		disconnect		callback for disconnection [IrcConnection]
 * 		ping			callback for ping [ IrcConnection, server ]
 * 		nick_change		callback for a nick change in the channel [ IrcConnection, oldNick, newNick ]
 * 		join			callback for a join [ cIrcConnection, channel, nick ]
 * 		part			callback for part [ IrcConnection, channel, nick, reason ]
 * 		privmsg			callback for message [ IrcConnection, channel, nick, ircmsg ]
 * 		notice			callback for notice [ IrcConnection, channel, nick, ircmsg ]
 * 		mode_change		callback for mode message [ IrcConnection, channel, changes(Array{nick, mode})]
 * 		topic_change	callback for topic change [ IrcConnection, channel, topic, nick ]
 * 		kick			callback for kick [ IrcConnection, channel, op, nick, reason ]
 * 		quit			callback for quick message [ IrcConnection, nick, reason ]
 * 		invite			callback for invite to channel [ IrcConnection, channel, op, nick ]
 * 		unhandled		callback for unhandled irc commands [ IrcConnection, parsed_irc_message ]
 * 		client			callback for client messages [IrcConnection, message]
 * 
 * *** Methods (Connection) ***
 * connect()				start irc connection (and sets nick and joins channels if those options have been passed)
 * disconnect()				closes the irc connection
 * is_connected()			checks to see if the socket is connected (returns boolean)
 * send_raw( data )			sends a string to the server, auto-appending \r\n (as per irc protocol)
 * 
 * *** Methods (IRC Protocols)
 * auth( u, r[, p] )		sends irc authentication data ot user (u = username, r = realname, p = optional server password)	
 * nick( newNick )			set a new nick
 * join( channel[,key] )	join a channel (and optionally with a key)
 * part( channel[,reason] )	part a channel (and optionally give a reason)
 * msg( channel, text )		send a message to a channel (or user)
 * note( channel, text )	send a notice to a channel (or user)
 * quit( [reason] )			sends a quit message to the server (with an optional reason)
 * invite( nick, channel )	sends an invite for channel to nick
 * away( [reason] )			sends an away message (if reason is blank, then you are back) 
 * topic( channel, text )	sends a topic message to change the topic of the specified channel
 * 
 * *** Methods (IRC Session Handling)
 * has_channel( channel )	checks to see if you are in a channel; a channel can be a nick if it's a private conversation
 * strip_nick_prefix( n )	Removes irc nick prefixes from name (like @^+%)
 * has_nick( nick )			checks if a user/nick is being tracked in the session
 * get_channel_list()		get a list of active channels in the session
 * get_nick_list( channel )	get a list of active nicks in the session
 * 
 * NOTE:	There are many a few other methods that are automatically ran by the client before
 * 			any of the callbacks are ran, so you'll have the most up-to-date information when
 * 			your callback is used.
 */

function newIrcConnection( host, port, options ) {
	var default_callbacks = {
		connect: null,
		disconnect: null,
		ping: null,
		nick_change: null,
		join: null,
		part: null,
		privmsg: null,
		notice: null,
		mode_change: null,
		topic_change: null,
		kick: null,
		quit: null,
		invite: null,
		unhandled: null,
		client: null
	};
	var connection = {
		socket: null,
		dataOut: "",
		messageBuffer: "",
		pulse: null,
		session: {
			"host": host,
			"port": port,
			password: null,
			nick: null,
			channels: {},
			nicks: {},
			newline: null,
			auth_send: false,
			hooks: [],
			buffer: "",
			join_queue: [],
			ready: false
		},
		callback: $.extend( {}, default_callbacks, options.callbacks ),
		connect: function() {
			if( this.socket ) this.disconnect();
			this.socket = Titanium.Network.createTCPSocket( this.session.host, this.session.port );
			var self = this;
			this.socket.onRead(function(evt){
				for(i=0;i<evt.length;i++) self.session.buffer += evt.charAt(i);
				
				// Check for server newline
				if( !self.session.newline ) {
					if( self.session.buffer.indexOf("\r\n") ) {			// Standard (linux)
						self.session.newline = "\r\n";
					} else if( self.session.buffer.indexOf( "\n\r" ) ) {	// Non-standard
						self.session.newline = "\n\r";
					} else if( self.session.buffer.indexOf( "\r" ) ) {
						self.session.newline = "\r";		// Standard (OSX?)
					} else if( self.session.buffer.indexOf( "\n" ) ) {
						self.session.newline = "\n";		// Standard (Win32?)
					}
					if( !self.session.newline ) return;
				}
				
				
				var lastNL = self.session.buffer.lastIndexOf(self.session.newline);
				var toParse = self.session.buffer.substring( 0, lastNL );
				self.session.buffer = self.session.buffer.substring( lastNL + self.session.newline.length );
				
				// Cut off trailing newline characters
				//data = data.substring( 0, data.length - self.session.newline.length );
				var messages = toParse.split( self.session.newline );
				if ( messages.length ) {
					for(i=0;i<messages.length;i++) if( messages[i].length ) self._dispatch( messages[i] );
				}
				
			});
			this.socket.connect();
			
			setTimeout( function(){
				self.pulse = setInterval( function(){
					var mybuffer = ""
					if( self.dataOut.length ) {
						console.log( "Queued Data=" + self.dataOut );
						mybuffer = self.dataOut.substring( 0, MAX_BUFFER_SIZE );
					}
					if( mybuffer.length ) {
						var bytes = self.socket.write( mybuffer );
						self.dataOut = self.dataOut.substring( mybuffer.length );
					}
				});
			}, 2000);
			
			if( this.callback.connect ) this.callback.connect( this );
		},
		disconnect: function( cb ) {
			this.quit("Disconnecting");
			setTimeout(function(){
				if( this.pulse ) clearInterval( this.pulse );
				if( this.socket ) this.socket.close();
				this.socket = null;
				this.pulse = null;
				this.dataOut = "";
				this.messageBuffer = "";
				this.session.newline = null;
				this.session.channels = {};
				this.session.nicks = {};
				this.session.buffer = "";
				this.session.join_queue = [];
				if( this.callback.disconnect ) this.callback.disconnect( this );
				if( cb ) cb();
			}, 500);
		},
		is_connected: function() {
			return !this.socket.isClosed();
		},
		send_raw: function( data ) {
			this.dataOut += data + "\r\n";
		},
		pass: function( password ) {
			this.send_raw( "PASS " + password );
		},
		user: function( username, realname ) {
			var user = username || "DevTK_User";
			var real = realname || "*";
			this.send_raw( "USER " + user + " * * :" + realname )
		},
		nick: function( newnick ) {
			this.send_raw( "NICK " + newnick );
			this.session.nick = newnick;
		},
		join: function( channel, key ) {
			//if( this.session.channels.indexOf(channel) ) return;
			if( this.session.ready ) {
				if( channel.indexOf( "&" ) && !key && this.callback.client ) { this.callback.client( this, "Cannot join '" + channel + "' without a key!" ); return; }
				var k = ( key ) ? " " + key : "";
				this.send_raw( "JOIN " + channel + k );
				//this.session.channels.push( channel );
				this.add_channel( channel );
				this.add_nick( this.session.nick, channel );
			} else {
				this.session.join_queue.push( { chan: channel, pass: key } );
			}
		},
		part: function( channel, reason ) {
			//if( !this.session.channels.indexOf(channel) ) return;
			this.send_raw( "PART " + channel + " :" + ( reason || "No reason" ) );
			this.rem_channel( channel );
		},
		msg: function( channel, message ) {
			this.send_raw( "PRIVMSG " + channel + " :" + message );
		},
		note: function( channel, message ) {
			this.send_raw( "NOTICE " + channel + " :" + message );
		},
		quit: function( reason ) {
			this.send_raw( "QUIT :" + ( reason || "No reason" ) );
		},
		invite: function( nick, channel ) {
			this.send_raw( "INVITE " + nick + " " + channel );
		},
		away: function( reason ) {
			if( reason ) {
				this.send_raw( "AWAY :" + reason );
			} else {
				this.send_raw( "AWAY" );
			}
		},
		topic: function( channel, text ) {
			this.send_raw( "TOPIC " + channel + " :" + text );
		},
		
		_dispatch: function( msg ) {
			var parsed = parse_irc( msg );
			console.log( "IRC=" + msg );
			// Deal with data appropriately
			switch( parsed.command.toUpperCase() ) {
			case "PING":
				this.send_raw( "PONG " + parsed.params[parsed.trailing] );
				if( this.callback.ping ) this.callback.ping( this, parsed.params[parsed.trailing] );
				break;
			case "PRIVMSG":
				if( this.callback.privmsg ) this.callback.privmsg( this, parsed.params[0], parsed.from, parsed.params[parsed.trailing] );
				break;
			case "NOTICE":
				if( this.callback.notice ) this.callback.notice( this, parsed.params[0], parsed.from, parsed.params[parsed.trailing] );
				break;
			case "JOIN":
				this.add_nick( parsed.from.nick, parsed.params[0] );
				if( this.callback.join ) this.callback.join( this, parsed.params[0], parsed.from );
				break;
			case "PART":
				this.rem_nick( parsed.from.nick, parsed.params[0] );
				if( this.callback.part ) this.callback.part( this, parsed.params[0], parsed.from, parsed.params[parsed.trailing] );
				break;
			case "MODE":
			case "UMODE":
				// Skipping for now...
				break;
			case "TOPIC":
				if( this.callback.topic ) this.callback.topic( this, parsed.params[0], parsed.params[parsed.trailing], parsed.from );
				break;
			case "QUIT":
				if( this.callback.quit ) this.callback.quit( this, parsed.from, parsed.params[parsed.trailing] );
				break;
			case "KICK":
				if( this.callback.kick ) this.callback.kick( this, parsed.params[0], parsed.from, parsed.params[1], parsed.params[parsed.trailing] );
				break;
			case "INVITE":
				if( this.callback.invite ) this.callback.invite( this, parsed.params[0], parsed.from, parsed.params[1], parsed.params[2] );
				break;
			case "NICK":
				if( this.callback.nick ) this.callback.nick( this, parsed.from.nick, parsed.params[parsed.trailing] );
				break;
			default:
				if( parsed.command == NUMERIC["ERR_NICKNAMEINUSE"] ) this.nick( this.session.nick + "_" );
				if( parsed.command == NUMERIC["ERR_ERRONEUSNICKNAME"] ) this.nick( "DevTK_User" );
				if( parsed.command == NUMERIC["RPL_TOPIC"] ) {
					this.set_channel_topic( parsed.params[1], parsed.params[ parsed.trailing ] );
				}
				if( parsed.command == NUMERIC["RPL_ENDOFMOTD"] || parsed.command == NUMERIC["ERR_NOMOTD"] ) {
					this.session.ready = true;
					for(c=0;c<this.session.join_queue.length;c++) this.join( this.session.join_queue[c].chan, this.session.join_queue[c].pass );
				}
				if( this.callback.unhandled ) this.callback.unhandled( this, parsed );
			}
		},
		has_channel: function( channel ) {
			if( !channel || !channel.length ) return false;
			if( this.session.channels[channel] ) return true;
			return false;
		},
		add_channel: function( channel ) {
			if( this.has_channel( channel ) ) return;
			this.session.channels[channel] = {};
		},
		rem_channel: function( channel ) {
			this.session.channels[channel] = null;
			for( u in this.session.nicks ) {
				this.rem_nick( u, channel );
			}
		},
		get_channel_topic: function( channel ) {
			console.log( this.session.channels[channel] );
			if( this.has_channel( channel ) ) return this.session.channels[channel].topic;
			return "";
		},
		set_channel_topic: function( channel, text ) {
			if( !this.has_channel( channel ) ) this.add_channel( channel );
			this.session.channels[channel].topic = text;
			console.log( "Setting " + channel + " topic to :" + this.session.channels[channel].topic );
		},
		strip_nick_prefix: function( nick ) {
			var NICK_PREFIXES = "~&@%+";
			var status = NICK_PREFIXES.indexOf( nick[0] );
			if( status >= 0 ) return nick.substring(1);
			return nick;
		},
		has_nick: function( nick ) {
			if( !nick || !nick.length ) return false;
			var stripped = this.strip_nick_prefix( nick );
			if( this.session.nicks[stripped] ) return true;
			return false;
		},
		add_nick: function( nick, channel ) {
			var stripped = this.strip_nick_prefix( nick );
			if( !this.has_nick( stripped ) ) {
				this.session.nicks[stripped] = [];
			}
			if( channel ) {
				this.session.nicks[stripped].push( channel );
				this.add_channel( channel );
			}
		},
		rem_nick: function( nick, channel ) {
			var stripped = this.strip_nick_prefix( nick );
			if( !this.has_nick( stripped ) ) return;
			if( channel ) {
				var cid = this.session.nicks[stripped].indexOf( channel );
				if( cid >= 0 ) {
					this.session.nicks[stripped].splice(cid, 1);
				}
				if( this.session.nicks[stripped].length == 0 ) this.session.nicks[stripped] = null;
			} else {
				this.session.nicks[stripped] = null;
			}
		},
		get_channel_list: function() {
			var list = [];
			for( chan in this.session.channels ) {
				list.push( chan );
			}
			return list;
		},
		get_nick_list: function( channel ) {
			var list = [];
			for( nick in this.session.nicks ) {
				if( channel && channel.length ) {
					if( this.session.nicks[nick].indexOf(channel) >= 0 ) list.push( nick );
				} else {
					list.push( nick );
				}
			}
			return list;
		}
	}
	return connection;
}

/*
 *	IRC Utility Functions
 */

function parse_irc( message ) {
		var irc_raw = message;
		var from_user = { };
		var irc_command = "";
		var irc_options = [];
		var irc_message = "";
		var startAt = 0;
		
		var token_end = message.indexOf( ":", 1 );
		if( token_end < 0 ) {
			token_end = message.length;
			irc_message = "";
		} else {
			irc_message = message.substr( token_end + 1 );
		}
		
		var offset = 0;
		var tokens = message.substring( 0, token_end ).split( " " );
		if( tokens[0].indexOf(":") >= 0 ) {
			from_user.raw = tokens[0].substring(1);
			var utokens = from_user.raw.split("!");
			from_user.nick = utokens[0];
			from_user.network = utokens[1];
			offset = 1;
		}
		
		irc_command = tokens[0 + offset];
		irc_options = tokens.slice(1+offset);
		if( irc_message.length > 0 ) irc_options.push( irc_message );
		
		return {
			"raw": irc_raw,
			"from": from_user,
			"command": irc_command,
			"params": irc_options,
			"trailing": irc_options.length-1
		};
}
